/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { ArkFile } from "arkanalyzer/lib";
import { BaseChecker, BaseMetaData } from "../BaseChecker";
import Logger, { LOG_MODULE_TYPE } from "arkanalyzer/lib/utils/logger";
import { RuleListUtil } from "../../utils/common/DefectsList";
import { Defects, IssueReport } from "../../model/Defects";
import { Rule } from "../../model/Rule";
import { FileMatcher, MatcherCallback, MatcherTypes } from "../../Index";
import { RuleFix } from "../../model/Fix";

const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, "CommaSpacingCheck");

const gMetaData: BaseMetaData = {
  severity: 1,
  ruleDocPath: "docs/comma-spacing-check.md",
  description: "Enforce consistent spacing before and after commas",
};

interface CommaSpacingOption {
  before?: boolean;
  after?: boolean;
}

export class CommaSpacingCheck implements BaseChecker {
  readonly metaData: BaseMetaData = gMetaData;
  public rule: Rule;
  public defects: Defects[] = [];
  public issues: IssueReport[] = [];
  private arkFile: ArkFile;

  private options: CommaSpacingOption = {
    before: false,
    after: true,
  };

  private issueMap: Map<string, IssueReport> = new Map();
  private inMultiLineComment: boolean = false;

  public check(target: any): void {
    this.options = this.rule.option[0] ? this.rule.option[0] : this.options;
    if (target instanceof ArkFile) {
      this.arkFile = target;
      const code = target.getCode();
      if (!code) {
        return;
      }

      const lines = code.split("\n");
      for (let i = 0; i < lines.length; i++) {
        const line = lines[i];
        this.checkLine(line, i + 1, target.getFilePath());
      }
    }
  }

  public registerMatchers(): MatcherCallback[] {
    const matcher: FileMatcher = {
      matcherType: MatcherTypes.FILE,
    };

    const matchCallback: MatcherCallback = {
      matcher: matcher,
      callback: this.check.bind(this),
    };

    return [matchCallback];
  }

  private checkLine(line: string, lineNumber: number, filePath: string): void {
    if (line.trim().startsWith("//") || line.trim().startsWith("/*")) {
      if (line.trim().startsWith("/*") && !line.includes("*/")) {
        this.inMultiLineComment = true;
      }
      return;
    }

    if (this.inMultiLineComment) {
      if (line.includes("*/")) {
        this.inMultiLineComment = false;
      }
      return;
    }

    let effectiveLine = line;
    let skipRanges: [number, number][] = [];

    let start = 0;
    while (true) {
      const commentStart = effectiveLine.indexOf("/*", start);
      if (commentStart === -1) break;

      const commentEnd = effectiveLine.indexOf("*/", commentStart);
      if (commentEnd === -1) {
        skipRanges.push([commentStart, effectiveLine.length]);
        this.inMultiLineComment = true;
        break;
      }

      skipRanges.push([commentStart + 2, commentEnd]); 
      start = commentEnd + 2;
    }

    const singleLineComment = effectiveLine.indexOf("//");
    if (singleLineComment !== -1) {
      skipRanges.push([singleLineComment + 2, effectiveLine.length]);
    }

    let match;
    const commaRegex = /,/g;
    while ((match = commaRegex.exec(effectiveLine)) !== null) {
      const commaIndex = match.index;

      const isInCommentContent = skipRanges.some(
        ([start, end]) => commaIndex > start && commaIndex < end
      );
      if (isInCommentContent) {
        continue;
      }

      let prevChar = effectiveLine[commaIndex - 1];
      let nextChar = effectiveLine[commaIndex + 1];

      const isBeforeComment = skipRanges.some(([start]) => start === commaIndex + 1);
      const isAfterComment = skipRanges.some(([_, end]) => end === commaIndex - 1);

      if (isBeforeComment) {
        nextChar = ' '; 
      }
      if (isAfterComment) {
        prevChar = ' ';
      }

      const nextCommaIndex = effectiveLine.indexOf(",", commaIndex + 1);
      const prevCommaIndex = effectiveLine.lastIndexOf(",", commaIndex - 1);

      const isInArray = effectiveLine.includes("[") && effectiveLine.includes("]");
      
      const isEmptyArrayElement =
        isInArray &&
        ((effectiveLine.slice(0, commaIndex).trim().endsWith("[") &&
          !effectiveLine.slice(commaIndex + 1).trim().startsWith("]")) ||
          (nextCommaIndex !== -1 &&
            effectiveLine.substring(commaIndex + 1, nextCommaIndex).trim() === "") ||
          (prevCommaIndex !== -1 &&
            effectiveLine.substring(prevCommaIndex + 1, commaIndex).trim() === ""));

      const isTrailingComma = isInArray && 
        effectiveLine.slice(commaIndex + 1).trim().replace(/\s*\/\*[\s\S]*?\*\/\s*/g, '').replace(/\s*\/\/.*/g, '').startsWith("]");

      const isConsecutiveComma =
        isEmptyArrayElement ||
        (nextCommaIndex !== -1 &&
          effectiveLine.substring(commaIndex + 1, nextCommaIndex).trim() ===
            "") ||
        (prevCommaIndex !== -1 &&
          effectiveLine.substring(prevCommaIndex + 1, commaIndex).trim() ===
            "");

      const isLineStart = effectiveLine.trimStart().startsWith(",");

      const isAfterLeftBracket =
        prevChar === "[" ||
        (prevChar === " " &&
          effectiveLine.slice(0, commaIndex).trim().endsWith("["));
      const isBeforeRightBracket =
        (nextChar === "]" ||
          (nextChar === " " &&
            effectiveLine
              .slice(commaIndex + 1)
              .trim()
              .startsWith("]"))) &&
        !isTrailingComma;
      const isBeforeRightBrace =
        nextChar === "}" ||
        (nextChar === " " &&
          effectiveLine
            .slice(commaIndex + 1)
            .trim()
            .startsWith("}"));
      const isBeforeRightParen =
        nextChar === ")" ||
        (nextChar === " " &&
          effectiveLine
            .slice(commaIndex + 1)
            .trim()
            .startsWith(")"));

      const hasSpaceBefore = prevChar === " ";
      const hasSpaceAfter = nextChar === " ";

      if (
        isConsecutiveComma ||
        isAfterLeftBracket ||
        (isBeforeRightBracket && !isTrailingComma) ||
        isBeforeRightBrace ||
        isBeforeRightParen ||
        isEmptyArrayElement
      ) {
        continue;
      }

      const actualColumn = commaIndex;
      const absolutePos = this.calculateAbsolutePosition(lineNumber, commaIndex);

      if (!isLineStart) {
        if (this.options.before && !hasSpaceBefore) {
          this.addDefect(
            "A space is required before ','",
            lineNumber,
            actualColumn + 1,
            filePath,
            absolutePos,
            absolutePos
          );
        }
        if (!this.options.before && hasSpaceBefore) {
          this.addDefect(
            "There should be no space before ','",
            lineNumber,
            actualColumn + 1,
            filePath,
            absolutePos - 1,
            absolutePos
          );
        }
      }

      if (this.options.after && !hasSpaceAfter && !isTrailingComma) {
        this.addDefect(
          "A space is required after ','",
          lineNumber,
          actualColumn + 1,
          filePath,
          absolutePos,
          absolutePos + 1
        );
      }
      if (!this.options.after && hasSpaceAfter && !isTrailingComma) {
        this.addDefect(
          "There should be no space after ','",
          lineNumber,
          actualColumn + 1,
          filePath,
          absolutePos + 1,
          absolutePos + 2
        );
      }
    }
  }

  private calculateAbsolutePosition(
    lineNumber: number,
    columnIndex: number
  ): number {
    if (!this.arkFile) {
      return columnIndex;
    }

    const fileContent = this.arkFile.getCode();
    if (!fileContent) {
      return columnIndex;
    }

    const lines = fileContent.split("\n");
    let position = 0;

    for (let i = 0; i < lineNumber - 1; i++) {
      position += lines[i].length + 1;
    }

    position += columnIndex;
    return position;
  }

  private addDefect(
    message: string,
    line: number,
    column: number,
    filePath: string,
    startPos: number,
    endPos: number
  ): void {
    const fixKey = `${line}%${column}%${this.rule.ruleId}`;
    const mergeKey = `${filePath}%${fixKey}%${message}`;

    const severity = this.rule.alert ?? this.metaData.severity;

    const defect = new Defects(
      line,
      column,
      message,
      severity,
      this.rule.ruleId,
      mergeKey,
      this.metaData.ruleDocPath,
      true,
      false,
      true,
      fixKey,
      true
    );

    let fix: RuleFix;
    if (message.includes("required after")) {
      fix = { range: [startPos, startPos + 1], text: ", " };
    } else if (message.includes("required before")) {
      fix = { range: [startPos, startPos + 1], text: " ," };
    } else {
      fix = { range: [startPos, endPos], text: "" };
    }

    const issue = new IssueReport(defect, fix);
    this.issueMap.set(fixKey, issue);
    this.issues.push(issue);
    RuleListUtil.push(defect);
  }
}
